/*
 *  Copyright 2001-2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package ar.uba.fi.posgrado.economy.service.impl;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.apache.log4j.Logger;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import ar.uba.fi.posgrado.academy.model.Body;
import ar.uba.fi.posgrado.common.dao.DAOException;
import ar.uba.fi.posgrado.common.exception.MissingArgumentException;
import ar.uba.fi.posgrado.common.exception.PosgradoException;
import ar.uba.fi.posgrado.common.exception.PosgradoRuntimeException;
import ar.uba.fi.posgrado.common.model.User;
import ar.uba.fi.posgrado.common.util.CriterionBuilder;
import ar.uba.fi.posgrado.common.util.ExpressionOperator;
import ar.uba.fi.posgrado.economy.dao.CollectDAO;
import ar.uba.fi.posgrado.economy.dao.CollectTypeDAO;
import ar.uba.fi.posgrado.economy.exception.CollectRuleException;
import ar.uba.fi.posgrado.economy.model.Collect;
import ar.uba.fi.posgrado.economy.model.CollectType;
import ar.uba.fi.posgrado.economy.model.Customer;
import ar.uba.fi.posgrado.economy.model.CustomerAccount;
import ar.uba.fi.posgrado.economy.model.Debt;
import ar.uba.fi.posgrado.economy.model.DebtStatus;
import ar.uba.fi.posgrado.economy.service.CollectService;
import ar.uba.fi.posgrado.economy.service.CustomerService;
import ar.uba.fi.posgrado.economy.service.DebtService;

/**
 * Servicio concreto para {@link Collect cobro} y {@link CollectType tipos de cobro}.<br>
 * Requiere los siguientes servicios:<br>
 * <ul>
 *  <li> {@link DebtService} </li>
 *  <li> {@link CustomerService} </li>
 * </ul>
 * 
 * @author nicolas.gonzalez
 * @see CollectService
 */
public class CollectServiceImpl implements CollectService {

    /** Logger */
    private static Logger logger = Logger.getLogger(CollectServiceImpl.class);
    
    private CollectDAO collectDAO;
    private CollectTypeDAO collectTypeDAO;
    
    private DebtService debtService;
    private CustomerService customerService;
    
    
    /** IoC */
    public void setCollectDAO(CollectDAO collectDAO) {
        this.collectDAO = collectDAO;
    }

    /** IoC */
    public void setCollectTypeDAO(CollectTypeDAO collectTypeDAO) {
        this.collectTypeDAO = collectTypeDAO;
    }

    /** IoC */
    public void setDebtService(DebtService debtService) {
        this.debtService = debtService;
    }

    /** IoC */
    public void setCustomerService(CustomerService customerService) {
        this.customerService = customerService;
    }

    /**
     * Valida para la creacion y/o modificacion.<br>
     * @param collect
     * @throws MissingArgumentException
     */
    private void validateCreateOrUpdate(Collect collect) throws MissingArgumentException {
        if (collect.getUser() == null) {
            throw new MissingArgumentException( "El usuario del cobro " +
                                                "no puede ser nulo");
        }
        if (collect.getCustomer() == null) {
            throw new MissingArgumentException( "El cliente del cobro " +
                                                "no puede ser nulo");
        }
        if (collect.getCollectType() == null) {
            throw new MissingArgumentException( "El tipo de cobro del cobro " +
                                                "no puede ser nulo");
        }
        if (collect.getDebt() == null) {
            throw new MissingArgumentException( "La deuda del cobro " +
                                                "no puede ser nulo");
        }
        if (collect.getDate() == null) {
            throw new MissingArgumentException( "La fecha del cobro " +
                                                "no puede ser nula");
        }
    }
    
    /**
     * @see ar.uba.fi.posgrado.economy.service.CollectService#create(ar.uba.fi.posgrado.economy.model.Collect, java.lang.Long, java.lang.Long)
     */
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    public void create(Collect collect, Long debtId, Long customerId)
            throws MissingArgumentException, CollectRuleException, PosgradoException {
        Debt debt = null;
        Customer customer = null;
        
        try {
            debt = this.debtService.getById(debtId);
            customer = this.customerService.getById(customerId);
        } catch (DAOException e) {
            throw new PosgradoException(
                    "Excepcion buscando la deuda y cliente antes de crear el cobro", e);
        }
        
        collect.setDebt(debt);
        collect.setCustomer(customer);

        if (logger.isDebugEnabled()) {
            logger.debug("Invocando a create habiendo encontrado la deuda y cliente ...");
        }

        this.create(collect, false, null);
    }
    
    /**
     * @see ar.uba.fi.posgrado.economy.service.CollectService#create(ar.uba.fi.posgrado.economy.model.Collect, java.lang.Long, java.lang.Long, java.lang.Double)
     */
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    public void create(Collect collect, Long debtId, Long customerId,
            Double amountFromAccount) throws    MissingArgumentException,
                                                CollectRuleException, 
                                                PosgradoException {
        Debt debt = null;
        Customer customer = null;
        
        try {
            debt = this.debtService.getById(debtId);
            customer = this.customerService.getById(customerId);
        } catch (DAOException e) {
            throw new PosgradoException(
                    "Excepcion buscando la deuda y cliente antes de crear el cobro", e);
        }
        collect.setDebt(debt);
        collect.setCustomer(customer);

        CustomerAccount account = customer.getAccount();
        Double maximum = this.getMaximumAvailable(account);
        if (maximum < amountFromAccount) {
            throw new CollectRuleException( "Intentando usar: " + amountFromAccount + " pesos de " +
                                            "la cuenta corriente cuando solo se tienen disponibles: " +
                                            maximum + ". Cobro no permitido.");
        }
        /* Ahora se valida que si lo recibido alcanza para saldar la deuda 
         * entonces, lo que se pretende de usar de la cta cte es ridiculo.
         * Si con lo q das alcanza entonces no podes usar de lo q tenes xq
         * ya estas dando como para saldar la deuda y mas es innecesario.
         * Es como decir: "doy 100 para pagar 30 y usa 12 de lo q ya tenia..."
         * en ese caso q el cliente pague 88 y fin de la historia 
         */
        Double debtValue = this.debtService.getBalanceWithBenefitsAndDicounts(debt); 
        if (collect.getValue() > debtValue) {
            throw new CollectRuleException( "Intentando saldar una deuda con dinero suficiente " +
                                            "para hacerlo y a la vez usando dinero del saldo a favor " + 
                                            "de la cta cte. Si el dinero es suficiente, entonces " + 
                                            "no es necesario sacar de lo que se tenia. Por favor " +
                                            "revise los valores.");
        }
        
        this.create(collect, true, amountFromAccount);
    }

    /**
     * @see ar.uba.fi.posgrado.economy.service.CollectService#create(ar.uba.fi.posgrado.economy.model.Collect)
     */
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    public void create(Collect collect) throws  MissingArgumentException,
                                                CollectRuleException, 
                                                PosgradoException {
        this.create(collect, false, null);
    }
    
    /**
     * Cambia collect y sus referencias para el caso en q:<br>
     * <b>El valor del cobro es menor que el de la deuda.</b><br><br>
     * 
     * Se modifica la deuda dejando en el saldo de la misma la diferencia q queda
     * por saldar y en estado: {@link DebtStatus#Balanced Con Saldo}<br>
     * Si se usa dinero de la cuenta entonces se actualiza la cta cte con la diferencia de 
     * lo q habia menos lo q se uso (puede quedar en cero).<br>
     * 
     * @param collect
     * @param debtValue
     * @param collectAndAmountValue
     * @param useFromAccount
     * @param amountFromAccount
     */
    private void registerWhenCollectLowerThanDebt(Collect collect, Double debtValue, Double collectAndAmountValue, boolean useFromAccount, Double amountFromAccount) {
        Customer customer = collect.getCustomer();
        CustomerAccount account = customer.getAccount();
        Debt debt = collect.getDebt();
        
        if (useFromAccount) {
            logger.info("El credito a favor de la cta cte se decrementara en: " + amountFromAccount + 
                        " por haber sido utilizado para el cobro...");
            // Lo q habia menos lo usado 
            account.setInFavorCredit(account.getInFavorCredit() - amountFromAccount);
            /* Si la deuda era de 400, y se estan pagando 200 y usando 100 a favor entonces:
             * ammountFromAccount = 100 / collectAndAmountValue = 300
             * El total de la cta se reducira en 300 ('collectAndAmountValue') */
            account.setTotalDebt(account.getTotalDebt() - collectAndAmountValue);
        }
        else {
            /* Si la deuda era de 400, y se estan pagando 200 y entonces:
             * collectAndAmountValue = 200
             * El total de la cta se reducira en 200 ('collectAndAmountValue') */
            account.setTotalDebt(account.getTotalDebt() - collectAndAmountValue);
        }
        
        // El saldo queda con la diferencia EXACTA de lo NO saldado. 
        debt.setBalance(debtValue - collectAndAmountValue);
        debt.setDebtStatus(DebtStatus.Balanced);
    }

    /**
     * Cambia collect y sus referencias para el caso en q:<br>
     * <b>El valor del cobro es mayor que el de la deuda.</b><br><br>
     * 
     * Se modifica el saldo de la deuda dejando saldo cero y estado:
     * {@link DebtStatus#Paid Pagada}.<br>
     * 
     * El sobrante de dinero va al saldo a favor de la cta cte.<br>
     * 
     * @param collect
     * @param debtValue
     * @param collectAndAmountValue
     */
    private void registerWhenCollectGreaterThanDebt(Collect collect, Double debtValue, Double collectAndAmountValue) {
        /* Se puede estar seguro q no se usa de la cuenta xq no esta permitido.
         * Ver create(Collect collect, Long debtId, Long customerId, Double amountFromAccount) */
        Customer customer = collect.getCustomer();
        CustomerAccount account = customer.getAccount();
        Debt debt = collect.getDebt();
        Double difference = collectAndAmountValue - debtValue;
        
        debt.setBalance(0d);
        debt.setDebtStatus(DebtStatus.Paid);
        account.setInFavorCredit(account.getInFavorCredit() + difference);
        // Se resta el total de la deuda del saldo total. La diferencia, arriba esta esto, fue al credito a favor
        account.setTotalDebt(account.getTotalDebt() - debtValue);
    }

    /**
     * Cambia collect y sus referencias para el caso en q:<br>
     * <b>El valor del cobro es igual al de la deuda.</b><br><br>
     * 
     * Se modifica el saldo de la deuda dejando saldo cero y estado:
     * {@link DebtStatus#Paid Pagada}.<br>
     * Si se usa dinero de la cuenta entonces se actualiza la cta cte con la diferencia de 
     * lo q habia menos lo q se uso (puede quedar en cero).<br>
     * 
     * @param collect
     * @param debtValue
     * @param collectAndAmountValue
     * @param useFromAccount
     * @param amountFromAccount
     */
    private void registerWhenCollectEqualsThanDebt(Collect collect, Double debtValue, Double collectAndAmountValue, boolean useFromAccount, Double amountFromAccount) {
        Customer customer = collect.getCustomer();
        CustomerAccount account = customer.getAccount();
        Debt debt = collect.getDebt();
        
        if (useFromAccount) {
            logger.info("El credito a favor de la cta cte se decrementara en: " + amountFromAccount + 
                        " por haber sido utilizado para el cobro...");
            // Lo q habia menos lo usado 
            account.setInFavorCredit(account.getInFavorCredit() - amountFromAccount);
        }
        debt.setBalance(0d);
        debt.setDebtStatus(DebtStatus.Paid);
        
        account.setTotalDebt(account.getTotalDebt() - collectAndAmountValue);
    }


    /**
     * Esta es la implementacion VERDADERA.<br>
     * El resto llama a esta.<br>
     * Previamente se valida si se tiene amountFromAccount disponible en la cta cte.<br>
     * @param collect
     * @param useFromAccount
     * @param amountFromAccount 
     * @throws MissingArgumentException
     * @throws CollectRuleException
     * @throws PosgradoException
     */
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    private void create(Collect collect, boolean useFromAccount, Double amountFromAccount) throws   MissingArgumentException, 
                                                                                                    CollectRuleException, 
                                                                                                    PosgradoException {
        // Validaciones basicas
        this.validateCreateOrUpdate(collect);
    
        Debt debt = collect.getDebt();
        Double debtValue = this.debtService.getBalanceWithBenefitsAndDicounts(debt);
        Double collectAndAmountValue = null;
        if (useFromAccount) {
            collectAndAmountValue = collect.getValue() + amountFromAccount;
            logger.info("Por registrar un cobro de valor: " + collect.getValue() +
                        ", utilizando: " + amountFromAccount + " de la cta cte q estaba a favor" +
                        ", sobre una deuda de valor (menos desc y benef): " + debtValue + " ... ");
        }
        else {
            collectAndAmountValue = collect.getValue();            
            logger.info("Por registrar un cobro de valor: " + collectAndAmountValue + 
                        ", sobre una deuda de valor (menos desc y benef): " + debtValue + ". No " +
                        " se utiliza dinero a favor de la cta cte...");
        }
        int option = collectAndAmountValue.compareTo(debtValue);
        if (option < 0) {
            logger.info("El valor del cobro es menor que el de la deuda. Quedara saldo en la deuda y quedara en estado Con Saldo.");
            this.registerWhenCollectLowerThanDebt(collect, debtValue, collectAndAmountValue, useFromAccount, amountFromAccount);
        }
        else if (option > 0) {
            logger.info("El valor del cobro es mayor que el de la deuda. Quedara en estado Pagada, saldo cero y la diferencia ira a la cta cte.");
            this.registerWhenCollectGreaterThanDebt(collect, debtValue, collectAndAmountValue);
        }
        else if (option == 0) {
            logger.info("El valor del cobro es igual que el de la deuda. Quedara en estado Pagada y saldo cero.");
            this.registerWhenCollectEqualsThanDebt(collect, debtValue, collectAndAmountValue, useFromAccount, amountFromAccount);
        }

        // Los datos estan modificados. Se guarda...
        try {
            this.collectDAO.save(collect);
            logger.info("Cobro: " + collect + " creado con exito");
        } catch (DAOException e) {
            throw new PosgradoException("Excepcion al crear el cobro: "
                    + collect, e);
        }
    }

    /**
     * @see ar.uba.fi.posgrado.common.service.GenericService#delete(java.lang.Object)
     */
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    public void delete(Collect t) throws PosgradoRuntimeException, CollectRuleException, PosgradoException {
        // No esta permitido eliminar cobros.
        throw new CollectRuleException("Un cobro no puede ser eliminado. Operacion no permitida.");
    }

    /**
     * @see ar.uba.fi.posgrado.common.service.GenericService#getAll()
     */
    @Transactional(readOnly = true)
    public List<Collect> getAll() throws PosgradoRuntimeException,
            PosgradoException {
        try {
            List<Collect> response = this.collectDAO.findAll();
            logger.info("Cobros encontrados: " + response.size());
            return response;
        } catch (DAOException e) {
            throw new PosgradoException("Excepcion buscando cobros", e);
        }
    }

    /**
     * @see ar.uba.fi.posgrado.common.service.GenericService#getById(java.lang.Long)
     */
    @Transactional(readOnly = true)
    public Collect getById(Long id) throws PosgradoRuntimeException,
            PosgradoException {
        try {
            Collect response = (Collect) this.collectDAO.findById(id);
            logger.info("Cobro con Id: " + id + ", encontrado con exito");
            return response;
        } catch (DAOException e) {
            throw new PosgradoException(
                    "Excepcion buscando el Cobro con Id: " + id, e);
        }
    }

    /**
     * @see ar.uba.fi.posgrado.common.service.GenericService#update(java.lang.Object)
     */
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    public Collect update(Collect collect) throws PosgradoRuntimeException, CollectRuleException, PosgradoException {
        // No esta permitido modificar cobros.
        throw new CollectRuleException("Un cobro no puede ser modificado. Operacion no permitida.");
    }

    /**
     * @see ar.uba.fi.posgrado.economy.service.CollectService#getCollectsByConditions(ar.uba.fi.posgrado.economy.model.Collect)
     */
    @Transactional(readOnly = true)
    public List<Collect> getCollectsByConditions(Collect collect)
            throws PosgradoRuntimeException, PosgradoException {
        if (collect == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("Buscando cobros sin condiciones a aplicar a los criterios ...");
            }
            return this.getAll();
        }
        List<Collect> response = null;
        try {
            CriterionBuilder builder = new CriterionBuilder();
            if (collect.getCollectType() != null) {
                builder.add("collectType", collect.getCollectType());
            }
            if (collect.getCustomer() != null) {
                builder.add("customer", collect.getCustomer());
            }
            if (collect.getDate() != null) {
                builder.add("date", collect.getDate(), ExpressionOperator.LowerEqualsThan);
            }
            if (collect.getDebt() != null) {
                builder.add("debt", collect.getDebt());
            }
            if (collect.getUser() != null) {
                builder.add("user", collect.getUser());
            }
            
            response = this.collectDAO.findByCriterions(builder.toCriterions());

            // Ordena por apellido del alumno
            Collections.sort(response, new Comparator<Collect>() {
                    public int compare(Collect a, Collect b) {
                        Customer custA = a.getCustomer();
                        Customer custB = b.getCustomer();
                        
                        int custCompare = custA.getName().compareTo(custB.getName());
                        
                        if (custCompare == 0) {
                            // Se usa el segundo criterio = fecha
                            return (a.getDate().compareTo(b.getDate()));
                        }
                        else {
                            return custCompare;
                        }
                    }
                }
            );
            if (logger.isDebugEnabled()) {
                logger.debug("Resultado ordenado por cliente y fecha de cobro");
            }
            
        }
        catch (DAOException e) {
            throw new PosgradoException("Excepcion buscando cobros con criterios", e);
        }
        
        return response;
    }

    /**
     * @see ar.uba.fi.posgrado.economy.service.CollectService#getMaximumAvailable(ar.uba.fi.posgrado.economy.model.CustomerAccount)
     */
    public Double getMaximumAvailable(CustomerAccount account)
            throws PosgradoRuntimeException, PosgradoException {
        return account.getInFavorCredit();
    }

    /**
     * @see ar.uba.fi.posgrado.economy.service.CollectService#getAllCollectTypes()
     */
    @Transactional(readOnly = true)
    public List<CollectType> getAllCollectTypes()
            throws PosgradoRuntimeException, PosgradoException {
        try {
            List<CollectType> response = this.collectTypeDAO.findAll();
            logger.info("Tipos de Cobros encontrados: " + response.size());
            return response;
        } catch (DAOException e) {
            throw new PosgradoException("Excepcion buscando tipos de cobro", e);
        }
    }

    /**
     * @see ar.uba.fi.posgrado.economy.service.CollectService#getCollectTypeById(java.lang.Long)
     */
    @Transactional(readOnly = true)
    public CollectType getCollectTypeById(Long collectTypeId)
            throws PosgradoRuntimeException, PosgradoException {
        try {
            CollectType response = (CollectType) this.collectTypeDAO.findById(collectTypeId);
            logger.info("Tipo de cobro con Id: " + collectTypeId + ", encontrado con exito");
            return response;
        } catch (DAOException e) {
            throw new PosgradoException(
                    "Excepcion buscando el tipo de Cobro con Id: " + collectTypeId, e);
        }
    }

	/** 
	 * @see ar.uba.fi.posgrado.common.service.GenericService#filter(java.util.List, ar.uba.fi.posgrado.common.model.User)
	 */
	public List<Collect> filter(List<Collect> list, User user)
			throws PosgradoRuntimeException, PosgradoException {
		if (!user.hasBodyRole()) {
			return list;
		}
		final Body body = user.getBody();
		if (logger.isDebugEnabled()) {
			logger.debug(	"El usuario tiene el rol Ente. Filtrando la informacion exclusiva para el " +
							"ente: " + body);
		}
		
		CollectionUtils.filter(list, new Predicate() {
			public boolean evaluate(Object object) {
				Collect collect = (Collect)object;
				if (collect.getDebt().getCourse().getBody().equals(body)) {
					return true;
				}
				
				return false;
			}
		});
		
		return list;

	}
    
    
    
}
